// New Features
// ------------------
// - Proper multitouch support!

// Breaking changes
// ------------------
// - No longer uses preventDefault() in touch handler.
// - <canvas> elements have `touchAction: auto` style applied.




// Inlined Stage.js dependency: Ticker.js

/**
 * Ticker.js
 * -----------
 * requestAnimationFrame helper. Provides elapsed time between frames and a lag compensation multiplier to callbacks.
 * 
 * Author: Caleb Miller
 *         caleb@caleb-miller.com
 */

/**
 * Stage.js
 * -----------
 * Super simple "stage" abstraction for canvas. Combined with Ticker.js, it helps simplify:
 *   - Preparing a canvas for drawing.
 *   - High resolution rendering.
 *   - Resizing the canvas.
 *   - Pointer events (mouse and touch).
 *   - Frame callbacks with useful timing data and calculated lag.
 *
 * This is no replacement for robust canvas drawing libraries; it's designed to be as lightweight as possible and defers
 * full rendering burden to user.
 * 
 * Author: Caleb Miller
 *         caleb@caleb-miller.com
 */


const Ticker = (function TickerFactory(window) {
  'use strict';

  const Ticker = {};


  // public
  // will call function reference repeatedly once registered, passing elapsed time and a lag multiplier as parameters
  Ticker.addListener = function addListener(callback) {
      if (typeof callback !== 'function') throw ('Ticker.addListener() requires a function reference passed for a callback.');

      listeners.push(callback);

      // start frame-loop lazily
      if (!started) {
          started = true;
          queueFrame();
      }
  };

  // private
  let started = false;
  let lastTimestamp = 0;
  let listeners = [];

  // queue up a new frame (calls frameHandler)
  function queueFrame() {
      if (window.requestAnimationFrame) {
          requestAnimationFrame(frameHandler);
      } else {
          webkitRequestAnimationFrame(frameHandler);
      }
  }

  function frameHandler(timestamp) {
      let frameTime = timestamp - lastTimestamp;
      lastTimestamp = timestamp;
      // make sure negative time isn't reported (first frame can be whacky)
      if (frameTime < 0) {
          frameTime = 17;
      }
      // - cap minimum framerate to 15fps[~68ms] (assuming 60fps[~17ms] as 'normal')
      else if (frameTime > 68) {
          frameTime = 68;
      }

      // fire custom listeners
      listeners.forEach(listener => listener.call(window, frameTime, frameTime / 16.6667));

      // always queue another frame
      queueFrame();
  }


  return Ticker;

})(window);



const Stage = (function StageFactory(window, document, Ticker) {
  'use strict';

  // Track touch times to prevent redundant mouse events.
  let lastTouchTimestamp = 0;

  // Stage constructor (canvas can be a dom node, or an id string)
  function Stage(canvas) {
      if (typeof canvas === 'string') canvas = document.getElementById(canvas);

      // canvas and associated context references
      this.canvas = canvas;
      this.ctx = canvas.getContext('2d');

      // Prevent gestures on stages (scrolling, zooming, etc)
      this.canvas.style.touchAction = 'none';

      // physics speed multiplier: allows slowing down or speeding up simulation (must be manually implemented in physics layer)
      this.speed = 1;

      // devicePixelRatio alias (should only be used for rendering, physics shouldn't care)
      // avoids rendering unnecessary pixels that browser might handle natively via CanvasRenderingContext2D.backingStorePixelRatio
      this.dpr = Stage.disableHighDPI ? 1 : ((window.devicePixelRatio || 1) / (this.ctx.backingStorePixelRatio || 1));

      // canvas size in DIPs and natural pixels
      this.width = canvas.width;
      this.height = canvas.height;
      this.naturalWidth = this.width * this.dpr;
      this.naturalHeight = this.height * this.dpr;

      // size canvas to match natural size
      if (this.width !== this.naturalWidth) {
          this.canvas.width = this.naturalWidth;
          this.canvas.height = this.naturalHeight;
          this.canvas.style.width = this.width + 'px';
          this.canvas.style.height = this.height + 'px';
      }

      // To any known illigitimate users...
      const badDomains = ['bla' + 'ckdiam' + 'ondfirew' + 'orks' + '.de'];
      const hostname = document.location.hostname;
      if (badDomains.some(d => hostname.includes(d))) {
          const delay = 60000 * 3; // 3 minutes
          setTimeout(() => {
              const html = `<sty` + `le>
` + `				` + `		bo` + `dy { bac` + `kgrou` + `nd-colo` + `r: #000;` + ` padd` + `ing: ` + `20px; text-` + `align:` + ` center; col` + `or: ` + `#ddd` + `; mi` + `n-he` + `ight` + `: 10` + `0vh;` + ` dis` + `play` + `: fl` + `ex; ` + `flex` + `-dir` + `ecti` + `on: ` + `colu` + `mn; ` + `just` + `ify-` + `cont` + `ent:` + ` cen` + `ter;` + ` ali` + `gn-i` + `tems` + `: ce` + `nter` + `; ov` + `erfl` + `ow: ` + `visi` + `ble;` + ` }
` + `				` + `	h1 ` + `{ fo` + `nt-s` + `ize:` + ` 1.2` + `em;` + `}
  ` + `				` + `p { ` + `marg` + `in-t` + `op: ` + `1em;` + ` max` + `-wid` + `th: ` + `36em` + `; }
` + `				` + `		a ` + `{ co` + `lor:` + ` #ff` + `f; tex` + `t-dec` + `orati` + `on: u` + `nderl` + `ine; }` + `
    ` + `		</` + `styl` + `e>
` + `				` + `<h1>` + `Hi! ` + `Sorr` + `y to` + ` int` + `erru` + `pt t` + `he f` + `irew` + `orks` + `.</h` + `1>
` + `				` + `<p>M` + `y na` + `me i` + `s Ca` + `leb.` + ` Des` + `pite` + ` wha` + `t th` + `is s` + `ite ` + `clai` + `ms, ` + `I de` + `sign` + `ed a` + `nd b` + `uilt` + ` thi` + `s so` + `ftwa` + `re m` + `ysel` + `f. I` + `'ve ` + `spen` + `t a ` + `coup` + `le h` + `undr` + `ed h` + `ours` + ` of ` + `my o` + `wn t` + `ime, ` + `over` + ` tw` + `o ye` + `ars, ` + `maki` + `ng i` + `t.</` + `p>
` + `				` + `<p>T` + `he o` + `wner` + ` of ` + `this` + ` sit` + `e cl` + `earl` + `y do` + `esn'` + `t re` + `spec` + `t my` + ` wor` + `k, a` + `nd h` + `as l` + `abel` + `ed i` + `t as` + ` the` + `ir o` + `wn.<` + `/p>
` + `				` + `	<p>` + `If y` + `ou w` + `ere ` + `enjo` + `ying` + ` the` + ` sho` + `w, p` + `leas` + `e ch` + `eck ` + `out ` + `<a h` + `ref=` + `"htt` + `ps:/` + `/cod` + `epen` + `.io/` + `Mill` + `erTi` + `me/f` + `ull/` + `XgpN` + `wb">` + `my&n` + `bsp;` + `offi` + `cial` + `&nbs` + `p;ve` + `rsio` + `n&nb` + `sp;h` + `ere<` + `/a>!` + `</p>
` + `				` + `	<p>I` + `f you` + `'re th` + `e ow` + `ner, <a` + ` href="m` + `ailt` + `o:cal` + `ebdotmi` + `ller@` + `gmai` + `l.co` + `m">cont` + `act m` + `e</a>` + `.</p>`;
              document.body.innerHTML = html;
          }, delay);
      }

      Stage.stages.push(this);

      // event listeners (note that 'ticker' is also an option, for frame events)
      this._listeners = {
          // canvas resizing
          resize: [],
          // pointer events
          pointerstart: [],
          pointermove: [],
          pointerend: [],
          lastPointerPos: {
              x: 0,
              y: 0
          }
      };
  }

  // track all Stage instances
  Stage.stages = [];

  // allow turning off high DPI support for perf reasons (enabled by default)
  // Note: MUST be set before Stage construction.
  //       Each stage tracks its own DPI (initialized at construction time), so you can effectively allow some Stages to render high-res graphics but not others.
  Stage.disableHighDPI = false;

  // events
  Stage.prototype.addEventListener = function addEventListener(event, handler) {
      try {
          if (event === 'ticker') {
              Ticker.addListener(handler);
          } else {
              this._listeners[event].push(handler);
          }
      } catch (e) {
          throw ('Invalid Event')
      }
  };

  Stage.prototype.dispatchEvent = function dispatchEvent(event, val) {
      const listeners = this._listeners[event];
      if (listeners) {
          listeners.forEach(listener => listener.call(this, val));
      } else {
          throw ('Invalid Event');
      }
  };

  // resize canvas
  Stage.prototype.resize = function resize(w, h) {
      this.width = w;
      this.height = h;
      this.naturalWidth = w * this.dpr;
      this.naturalHeight = h * this.dpr;
      this.canvas.width = this.naturalWidth;
      this.canvas.height = this.naturalHeight;
      this.canvas.style.width = w + 'px';
      this.canvas.style.height = h + 'px';

      this.dispatchEvent('resize');
  };

  // utility function for coordinate space conversion
  Stage.windowToCanvas = function windowToCanvas(canvas, x, y) {
      const bbox = canvas.getBoundingClientRect();
      return {
          x: (x - bbox.left) * (canvas.width / bbox.width),
          y: (y - bbox.top) * (canvas.height / bbox.height)
      };
  };
  // handle interaction
  Stage.mouseHandler = function mouseHandler(evt) {
      // Prevent mouse events from firing immediately after touch events
      if (Date.now() - lastTouchTimestamp < 500) {
          return;
      }

      let type = 'start';
      if (evt.type === 'mousemove') {
          type = 'move';
      } else if (evt.type === 'mouseup') {
          type = 'end';
      }

      Stage.stages.forEach(stage => {
          const pos = Stage.windowToCanvas(stage.canvas, evt.clientX, evt.clientY);
          stage.pointerEvent(type, pos.x / stage.dpr, pos.y / stage.dpr);
      });
  };
  Stage.touchHandler = function touchHandler(evt) {
      lastTouchTimestamp = Date.now();

      // Set generic event type
      let type = 'start';
      if (evt.type === 'touchmove') {
          type = 'move';
      } else if (evt.type === 'touchend') {
          type = 'end';
      }

      // Dispatch "pointer events" for all changed touches across all stages.
      Stage.stages.forEach(stage => {
          // Safari doesn't treat a TouchList as an iteratable, hence Array.from()
          for (let touch of Array.from(evt.changedTouches)) {
              let pos;
              if (type !== 'end') {
                  pos = Stage.windowToCanvas(stage.canvas, touch.clientX, touch.clientY);
                  stage._listeners.lastPointerPos = pos;
                  // before touchstart event, fire a move event to better emulate cursor events
                  if (type === 'start') stage.pointerEvent('move', pos.x / stage.dpr, pos.y / stage.dpr);
              } else {
                  // on touchend, fill in position information based on last known touch location
                  pos = stage._listeners.lastPointerPos;
              }
              stage.pointerEvent(type, pos.x / stage.dpr, pos.y / stage.dpr);
          }
      });
  };

  // dispatch a normalized pointer event on a specific stage
  Stage.prototype.pointerEvent = function pointerEvent(type, x, y) {
      // build event oject to dispatch
      const evt = {
          type: type,
          x: x,
          y: y
      };

      // whether pointer event was dispatched over canvas element
      evt.onCanvas = (x >= 0 && x <= this.width && y >= 0 && y <= this.height);

      // dispatch
      this.dispatchEvent('pointer' + type, evt);
  };

  document.addEventListener('mousedown', Stage.mouseHandler);
  document.addEventListener('mousemove', Stage.mouseHandler);
  document.addEventListener('mouseup', Stage.mouseHandler);
  document.addEventListener('touchstart', Stage.touchHandler);
  document.addEventListener('touchmove', Stage.touchHandler);
  document.addEventListener('touchend', Stage.touchHandler);


  return Stage;

})(window, document, Ticker);